/*  *********************************************************************
    两个部件组装一个NTP时钟：ESP32—C3—MINI板 + 四位7脚LED时钟数码管
    编译环境 PlatformIO + VSCode， Arduino框架
    通过硬件PWM控制LED数码管整体亮度，启用IO限流实现各笔画字段亮度均匀
    *********************************************************************  */

#include <Arduino.h>
// #include <WiFi.h>
#include <WiFiManager.h>          //https://github.com/tzapu/WiFiManager
// #include "esp32-hal-ledc.h"
// #include <ledc_hal.h>
#include "esp_sntp.h"
#include "ledmap.h"

#define VER                     "0.2.0"
#define LED_BUILTIN             GPIO_NUM_8

// const char *ssid                = "ChinaNet-5678";
// const char *password            = "87654321";

const char *ntpServer1          = "time.ys7.com";           // 海康威视的NTP服务器
const char *ntpServer2          = "ntp.tencent.com";        // 腾讯的NTP
const char *ntpServer3          = "cn.pool.ntp.org";        // 国家服务器
// const char *ntpServer3          = "2.cn.pool.ntp.org";     // 国家服务器2
// const char *ntpServer1          = "time.ys7.com"           // 海康威视的NTP服务器
// const char *ntpServer1          = "ntp.ntsc.ac.cn"         // 中国科学院国家授时中心的NTP服务器
// const char *ntpServer1          = "time.nist.gov"          // 美国国家授时中心的NTP服务器
// const char *ntpServer1          = "ntp.aliyun.com";        // 阿里云

const long gmtOffset_sec        = 3600 * 8;                 // 东八区
const int daylightOffset_sec    = 0;                        // 不使用夏令时

// 2025-6-14, maidoo, 在ESP32-C3 Super Mini板上，连接了GPIO8导致芯片进入Download模式后CDC串口无响应，烧录失败。避免使用GPIO8
// 2025-7-20, GPIO9是BOOT按钮，PCB上有外置10K上拉电阻。不要用，否则数码管显示有鬼影。
// 以方便硬件引脚连接为优先。尽量避免boot Strapping引脚（GPIO2、GPIO8、GPIO9 for ESP32C3）。
const uint8_t led_pin_gpio[IO_NUM7] = {
    GPIO_NUM_20,        // 连接LED屏pin1 的GPIO引脚
    GPIO_NUM_10,        // 连接LED屏pin2 的GPIO引脚
    GPIO_NUM_3,
    GPIO_NUM_21,
    GPIO_NUM_7,
    GPIO_NUM_6,
    GPIO_NUM_5          // 连接LED屏pin7 的GPIO引脚
};

// const char *time_zone = "Asia/Shanghai";  // TimeZone rule for Europe/Rome including daylight adjustment rules (optional)
struct tm timeinfo;

WiFiManager wm;

//数码管显示数字 0~9 的段码译码值，10 为空显示
const uint8_t seven_seg_table[11] = {0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x00};

//数组中的每个数值，对应4位数码管中的一个数码管显示内容，
uint8_t display_contant[4];

// LED channel that will be used instead of automatic selection.
#define LEDC_CHANNEL            0
#define LEDC_CHANNEL0_FUNC      45

volatile int brightness =       55;             // how bright the LED is
int fadeAmount =                50;             // how many points to fade the LED by


void printLocalTime() {
    if (!getLocalTime(&timeinfo)) {
        Serial.println("No time available (yet)");
        return;
    }
    // 格式化字符串 "%A, %B %d %Y %H:%M:%S" 表示：星期几, 月份 日 年 小时:分钟:秒
    Serial.println(&timeinfo, "Now: %A, %B %d %Y %H:%M:%S");
}

// Callback function (gets called when time adjusts via NTP)
void timeavailable(struct timeval *t) {
    Serial.print("Got time adjustment from NTP, while my ip address: ");
    Serial.println(WiFi.localIP());
    printLocalTime();
}


/** *******************************************************************************************************
     * @brief 将所有LED控制引脚设置为高阻态（悬浮模式）
     * 该函数通过反向遍历LED控制引脚数组，将每个引脚设置为开漏输出模式并拉高电平，
     * 以此实现所有引脚的高阻态，避免引脚间的干扰，同时防止操作 `pinMode` 产生信号毛刺。
********************************************************************************************************* */
void led_gpio_clear(void) {
    uint8_t i;

    for (i = IO_NUM7; i ; i--) {
        // 统一清除IO引脚时，不再操作pinMode的切换，防止毛刺，引脚默认OD模式，
        // pinMode(led_pin_gpio[ i-1 ], INPUT);
        // pinMode(led_pin_gpio[ i-1 ], OUTPUT_OPEN_DRAIN);

        // 仅仅是把所有引脚设置为高阻态。开漏输出1，也是高阻态，等价悬浮
        digitalWrite(led_pin_gpio[ i-1 ], HIGH);
    }
}

/**
 * @brief 设置数码管冒号的显示状态。
 *
 * 该函数根据传入的参数 `ON` 来控制数码管冒号的点亮或熄灭，
 * 通过修改显示缓存 `display_cache` 中对应位置的值来实现。
 *
 * @param ON 一个布尔值，`1` 表示点亮冒号，`0` 表示熄灭冒号。
 */
void led_set_colon(uint8_t ON) {
    const uint8_t m = 28;       // 冒号在led_map表中第28个

    // 冒号正极在第7脚，负极在第1脚。数组下标就是减一后的 ledmap[6][0]
    if (ON) display_cache[led_map[m].pinA-1][led_map[m].pinK-1] = 1;    // 点亮冒号
    else    display_cache[led_map[m].pinA-1][led_map[m].pinK-1] = 0;    // 熄灭冒号
}

/**
 * @brief 刷新一帧LED显示数据，执行时间16毫秒。
 *
 * 该函数通过逐引脚扫描的方式，依次激活每个数码管控制引脚，
 * 根据显示缓存数据决定对应LED的亮灭，实现数码管的动态显示。
 * 最后会补充一个消隐时间，避免余晖影响显示效果。
 */
void refresh_led_one_frame(void) {
    uint8_t a, k;

    // 7个正极引脚逐个扫描，依次激活每个数码管控制引脚
    for(a = 0 ; a < IO_NUM7 ; a++) {

        // 先将所有引脚设置为高阻态，避免引脚间的干扰
        led_gpio_clear();

        // 再将当前引脚设置为高电平，即选中当前引脚，作为正极输出（推挽模式，作为正极公共极）
        pinMode     (led_pin_gpio[a], OUTPUT);
        digitalWrite(led_pin_gpio[a], HIGH);

        // 配置LEDC通道，控制PWM占空比来控制LED亮度
        pinMatrixOutAttach(led_pin_gpio[a], LEDC_CHANNEL0_FUNC, false, false);

        // 设置公共极驱动电流40mA： IO_MUX_GPIOn_FUN_DRV 选择管脚驱动强度。0：~5 mA；1：~10 mA；2：~20 mA；3：~40 mA。
        // GPIO_PAD_SET_DRV(led_pin_gpio[a], GPIO_DRIVE_CAP_3);
        PIN_SET_DRV(REG_IO_MUX_BASE +4 + 4*led_pin_gpio[a], GPIO_DRIVE_CAP_3);

        // 每个正极对应的6个负极引脚逐个扫描，处理每个点位
        for (k = 0; k < IO_NUM7; k++)  {
           if (display_cache[a][k] == 1)  {
                // 若当前显示点位需要点亮，则需要拉低对应的负极引脚（OD或者推挽模式都可以）。
                digitalWrite  (led_pin_gpio[k], LOW);
            }
            else if (display_cache[a][k] == 0) {
                // 若当前显示点位需要熄灭，将对应负极设置为高阻态，即OD输出模式下拉高
                // 前面 led_gpio_clear() 已经设置为默认的HIGH状态，这里可以减少重复操作
                // digitalWrite  (led_pin_gpio[k], HIGH);
            } else {
                // 其他的=3的值，是数组中a==k情况，无意义，无需任何操作
                // pinMode(led_pin_gpio[k], OUTPUT_OPEN_DRAIN);
            }
        }

        // 每个公共正极引脚扫描显示暂留2毫秒，一轮16毫秒（7个引脚 * 2毫秒 + 2ms），显示整体刷新速率约62赫兹。
        vTaskDelay(pdMS_TO_TICKS(2));

        // 拆除公共正极的PWM调制波
        ledcDetachPin(led_pin_gpio[a]);

        // 设置改为单个负极的5mA： IO_MUX_GPIOn_FUN_DRV 选择管脚驱动强度。0：~5 mA；1：~10 mA；2：~20 mA；3：~40 mA。
        // GPIO_PAD_SET_DRV(led_pin_gpio[a], GPIO_DRIVE_CAP_0);
        // GPIO_PAD_SET_DRV(5, GPIO_DRIVE_CAP_0);
        PIN_SET_DRV(REG_IO_MUX_BASE +4 + 4*led_pin_gpio[a], GPIO_DRIVE_CAP_0);

        // 扮演完毕的公共极，从推挽输出模式切换为OD开漏输出模式，默认状态为高阻态
        pinMode(led_pin_gpio[a], OUTPUT_OPEN_DRAIN);
        digitalWrite(led_pin_gpio[a], HIGH);

    }

    // 补充一个周期的消隐时间，熄灭所有段位，避免余晖影响显示效果
    led_gpio_clear();
    vTaskDelay(pdMS_TO_TICKS(2));
}

/***********************************************************************************************************
* @描述    :  根据缓存数据刷新LED亮灭状态
***********************************************************************************************************/
/*
 * @brief 根据显示缓存数据刷新LED的亮灭状态。
 *
 * 该函数通过逐引脚扫描的方式，依次激活每个数码管控制引脚，
 * 根据显示缓存数据决定对应LED的亮灭，实现数码管的动态显示。
 *
 * @param pvParameters 任务参数，当前未使用。
 * ************************************************************** */
void refresh_led_task(void *pvParameters) {
    uint8_t i = 0;
    while (1) {
        refresh_led_one_frame();    // 执行时间~16毫秒

        i++;
        if (i == 25) {
            // pinMode(LED_BUILTIN, INPUT);
            digitalWrite(LED_BUILTIN, HIGH);
            led_set_colon(0);       // 冒号闪烁
        }

        if (i >= 61) {
            // pinMode(LED_BUILTIN, OUTPUT_OPEN_DRAIN);
            i = 0;
            digitalWrite(LED_BUILTIN, LOW);
            led_set_colon(1);
        }
    }
}

/**
 * @brief 将4位数据解码为7x7数组格式，用于控制LED数码管显示。
 *
 * 该函数接收一个包含4位数据的数组和一个7x7显示缓存数组，
 * 将4位数据中的每个数字转换为对应的七段数码管段码，
 * 并根据 `led_map` 映射表将段码信息填充到显示缓存数组中。
 *
 * @param data4dot 指向包含4位待显示数据的数组的指针。
 * @param buff 7x7显示缓存数组，用于存储解码后的LED显示信息。
 */
void data4_to_7x7arr_decode(uint8_t *data4dot, display_cache_t buff) {
    uint8_t i, j, x;

    // 遍历待显示的4位数据
    for(i = 0; i < 4; i++) {
        // 遍历每个数字对应的七段笔画
        for(j = 0; j < SEG_NUM7; j++) {
            // 计算当前笔画在 led_map 中的索引
            x = SEG_NUM7 * i + j;
            // 检查当前数字的对应段码位是否为1
            if (seven_seg_table[data4dot[i]] & (0x01 << j))
                // 若为1，则将显示缓存中对应LED位置设置为点亮状态
                buff[led_map[x].pinA -1][led_map[x].pinK -1] = 1;
            else
                // 若为0，则将显示缓存中对应LED位置设置为熄灭状态
                buff[led_map[x].pinA -1][led_map[x].pinK -1] = 0;
        }
    }
}

/***********************************************************************************************************
* @描述    :  4位数据转换成LED段码
***********************************************************************************************************/
void led_show_data(uint8_t *buff) {
    // uint8_t i;
    // for(i = 0 ; i < 4 ; i++) {
    //     data_to_7seg_decode( &display_cache[i * SEG_NUM7] , buff[i]);
    // }

    data4_to_7x7arr_decode(buff, display_cache);
}


void setup() {

    // 一上电，先准备点灯
    pinMode(LED_BUILTIN, OUTPUT_OPEN_DRAIN);

    // 数码管显示四个“8”
    display_contant[0] = 8;
    display_contant[1] = 8;
    display_contant[2] = 8;
    display_contant[3] = 8;
    led_show_data(display_contant);

    // 设置同名脚的标志，特指正极的PWM调制输出的标志
    for (uint8_t j = 0; j < IO_NUM7; j++)  display_cache[j][j] = 3;

    // display_cache[28] = 1;  // 显示 冒号 图标
    // display_cache[6][0] = 1;  // 显示 冒号 图标
    led_set_colon(1);

    Serial.begin(115200);
    Serial.println("setup brightness: " + String(brightness));

    ledcSetup(LEDC_CHANNEL, 12000, 8); // 12 kHz PWM, 8-bit resolution
    // ledcAttachPin(led_pin_gpio[0], LEDC_CHANNEL); // assign RGB led pins to channels
    // ledcAttachPin(LED_BUILTIN, LEDC_CHANNEL);
    ledcWrite(LEDC_CHANNEL, brightness);


    // Create a task for refreshing the LED display
    xTaskCreate(
        refresh_led_task, // Task function
        "refresh_led", // Name of the task
        1024, // Stack size
        NULL, // Parameters (not used)
        1, // Priority
        NULL // Task handle
    );

    // Serial.println("After task create Free heap: " + String(ESP.getFreeHeap()));
    Serial.println (" ******************************");
    Serial.print   ("     AK Clock, Version: ");
    Serial.println (VER);
    Serial.println (" ******************************");
    Serial.print   (" Copyright(C) 2025 maidoo happy at mydigit.cn, All rights reserved.\r\n\r\n");
    delay(2000);

    // First step is to configure WiFi STA and connect in order to get the current time and date.
    // Serial.println("Connecting to SSID = " + String(ssid) + " ....");
    // WiFi.begin(ssid, password);

    wm.setTimeout(120);

    //fetches ssid and pass from eeprom and tries to connect
    //if it does not connect it starts an access point with the specified name
    //here  "AKClock_Setup"
    //and goes into a blocking loop awaiting configuration
    if(!wm.autoConnect("AKClock_Setup")) {
        Serial.println("failed to connect and hit timeout");
        delay(5000);
        //reset and try again, or maybe put it to deep sleep
        ESP.restart();
        delay(5000);
    }

    //if you get here you have connected to the WiFi
    Serial.println("connected...yeey :)");


  /**
   * NTP server address could be acquired via DHCP,
   *
   * NOTE: This call should be made BEFORE esp32 acquires IP address via DHCP,
   * otherwise SNTP option 42 would be rejected by default.
   * NOTE: configTime() function call if made AFTER DHCP-client run
   * will OVERRIDE acquired NTP server address
   */
    //    esp_sntp_servermode_dhcp(1);  // (optional)
    sntp_servermode_dhcp(1);  // (optional)

    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }
    Serial.println(" CONNECTED!");

    // set notification call-back function
    sntp_set_time_sync_notification_cb(timeavailable);

  /**
   * This will set configured ntp servers and constant TimeZone/daylightOffset
   * should be OK if your time zone does not need to adjust daylightOffset twice a year,
   * in such a case time adjustment won't be handled automagically.
   */
      configTime(gmtOffset_sec, daylightOffset_sec, ntpServer1, ntpServer2, ntpServer3);
//   configTzTime(time_zone, ntpServer1, ntpServer2, ntpServer3);

}


void loop() {

    if (!getLocalTime(&timeinfo)) {
        Serial.println("No time available (yet)");
    } else {
        display_contant[0] = timeinfo.tm_hour / 10;
        display_contant[1] = timeinfo.tm_hour % 10;
        display_contant[2] = timeinfo.tm_min  / 10;
        display_contant[3] = timeinfo.tm_min  % 10;

        led_show_data(display_contant);

        // 格式化字符串 "%A, %B %d %Y %H:%M:%S" 表示：星期几, 月份 日 年 小时:分钟:秒
        Serial.println(&timeinfo, "now: %A, %B %d %Y [%H:%M:%S]");
    }

    // 每5秒刷新一次时间
    vTaskDelay(pdMS_TO_TICKS(2000));

    if (Serial.available())  {
        brightness = Serial.parseInt();
    }

    // change the brightness for next time through the loop:
    // brightness = brightness + fadeAmount;

    // reverse the direction of the fading at the ends of the fade:
    // if (brightness <= 0 || brightness >= 255) {
    //     fadeAmount = -fadeAmount;
    // }

    ledcWrite(LEDC_CHANNEL, brightness);
    Serial.print("brightness=");
    Serial.println(brightness);

}
